package org.springframework.core.annotation;

import com.google.common.base.MoreObjects;
import lombok.Getter;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.AliasUtils.AliasChains;
import org.springframework.util.MethodKey;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Getter
public class AnnotationTypeMapping {

    private final Class<? extends Annotation> annotationType;
    private final List<AttributeMethod> attributeMethods;
    private final AliasChains<MethodKey> aliasChains;

    private AnnotationTypeMapping(Class<? extends Annotation> annotationType) {
        this.annotationType = annotationType;
        this.attributeMethods = Arrays.stream(annotationType.getDeclaredMethods()).map(method -> new AttributeMethod(method)).collect(Collectors.toList());
        this.aliasChains = resolveAliasGroups();
    }

    static AnnotationTypeMapping of(Class<? extends Annotation> annotationType) {
        return new AnnotationTypeMapping(annotationType);
    }

    private AliasChains<MethodKey> resolveAliasGroups() {
        AliasChains<MethodKey> groups = new AliasChains<>();
        for (AttributeMethod attributeMethod : attributeMethods) {
            MethodKey originalMethodKey = attributeMethod.getMethodKey();
            AliasFor aliasFor = attributeMethod.getAliasFor();
            if (aliasFor == null) {
                groups.merge(originalMethodKey);
                continue;
            }

            Class<?> aliasAnnotationType = aliasFor.annotation();
            aliasAnnotationType = Objects.equals(aliasAnnotationType, Annotation.class) ? attributeMethod.getDeclaringClass() : aliasAnnotationType;

            String attrName = StringUtils.isNotBlank(aliasFor.value()) ? aliasFor.value() : aliasFor.attribute();

            MethodKey aliasMethodKey = MethodKey.of(aliasAnnotationType, attrName);
            groups.merge(originalMethodKey, aliasMethodKey);
        }

        return groups;
    }

    @Getter
    class AttributeMethod {
        private Class<?> declaringClass;
        private Method method;
        private Object defaultValue;
        private AliasFor aliasFor;
        private MethodKey methodKey;

        @SneakyThrows
        private AttributeMethod(Method method) {
            this.methodKey = MethodKey.of(method);
            this.method = method;
            this.defaultValue = method.getDefaultValue();
            this.declaringClass = method.getDeclaringClass();

            AliasFor aliasFor = method.getDeclaredAnnotation(AliasFor.class);
            if (aliasFor != null) {
                this.aliasFor = aliasFor;
            }
        }

        public boolean isValueSettled(Object value) {
            return !Objects.equals(this.defaultValue, value);
        }

        @SneakyThrows
        public Object getValue(Annotation annotation) {
            return method.invoke(annotation);
        }

        @Override
        public String toString() {
            return MoreObjects
                    .toStringHelper(this)
                    .add("declaringClass", declaringClass.getSimpleName())
                    .add("method", method.getName())
                    .add("defaultValue", defaultValue)
                    .toString();
        }
    }

    @Override
    public String toString() {
        return MoreObjects
                .toStringHelper(this)
                .add("annotationType", annotationType.getSimpleName())
                .add("attributeMethods", attributeMethods.size())
                .add("aliasGroups", aliasChains.size())
                .toString();
    }
}
